
(function(){

  var namespace = 'App';

  var DOM = Basis.DOM;
  var curLocation = '';

  var cripSeed = 0;
  function getCRID(){
    return cripSeed++;
  }

  function logResult(message){
    App.Type.CommandResult({
      id: getCRID(),
      content: message
    });
  }

  function logCommand(command){
    App.Type.CommandResult({
      id: getCRID(),
      type: 'command',
      content: command
    });
  }

  function getLevel(levelName){
    var level = App.Type.Level.get(levelName);

    if (!level)
      throw levelName
        ? 'Заданный уровень переопределения ' + levelName + ' - не найден (его можно создать командой create level ' + levelName + ')'
        : 'Не задан уровень переопределения (параметр -l levelName)';

    return level;
  }

  function buildBemPath(env){
    var path = curLocation || '';

    if (env.tokens['-l'])
      path = (path ? path + '/' : '') + env.tokens['-l'][0].replace(/^\/+|\/+$/g, '');

    if (env.tokens['-b'])
      path += '/' + env.tokens['-b'][0];

    if (env.tokens['-e'])
      path += '__' + env.tokens['-e'][0];

    if (env.tokens['-m'])
      path += '_' + env.tokens['-m'][0];

    return path;
  }

  function getOwner(env){
    var path = buildBemPath(env);
    var object = App.Type.resolveBem(path);

    if (!object)
    {
      // определить тип и выбросить сообщение с комментариями
      console.log(path);
      throw 'Объект не найден (' + path + ')';
    }

    return object;
  }


  function names2decl(names){
    var tokens = names.sort();
    var result = [];
    var lastBlock;
    var lastElem;
    var lastMod;

    function putBlock(blockName){
      if (!lastBlock || lastBlock.name != blockName)
      {
        result.push(lastBlock = { name: blockName });
        lastElem = null;
        lastMod = null;
      }

      return lastBlock;
    }

    function putElem(token){
      var path = App.Type.name2parts(token);
      var elemName = path.pop().substr(2);
      var blockName = path.join('');

      putBlock(blockName);

      if (!lastElem || lastElem.name != elemName)
      {
        if (!lastBlock.elems)
          lastBlock.elems = [];

        lastBlock.elems.push(lastElem = { name: elemName });
        lastMod = null;
      }

      return lastElem;
    }
    //console.log(document.documentElement.innerHTML);

    for (var i = 0, token; token = tokens[i]; i++)
    {
      var type = App.Type.resolveBemType('/' + token).typeName;
      if (type == 'Block')
        putBlock(token);
      if (type == 'Elem')
        putElem(token);
      if (type == 'Mod')
      {
        var parts = App.Type.name2parts(token);
        var mod = parts.pop().substr(1).split(/_/);
        var parent = parts.join('')

        var modName = mod.shift();
        var modValue = mod.join('_');

        if (App.Type.resolveBemType('/' + parent).typeName == 'Block')
          parent = putBlock(parent);
        else
          parent = putElem(parent);

        if (!parent.mods)
          parent.mods = [];

        lastMod = parent.mods.search(modName, 'name');
        if (!lastMod)
          parent.mods.push(lastMod = { name: modName, vals: [] });

        lastMod.vals.push(modValue);
      }
    }

    return result;
  }

  function getNamesFromBemdecl(decl){
    function addMods(baseName, mods){
      for (var i = 0, mod; mod = mods[i++];)
        for (var j = 0, val; val = mod.vals[j++];)
          names[baseName + '_' + mod.name + '_' + val] = true;
    }

    function addNames(baseName, list){
      for (var i = 0, item; item = list[i++];)
      {
        var path = baseName + item.name;
        names[path] = true;

        if (item.elems)
          addNames(path + '__', item.elems);

        if (item.mods)
          addMods(path, item.mods);
      }
    }

    var names = {};
    addNames('', decl);
    return Object.keys(names);
  }

  function getNamesFromHtml(html, path, callback){
    var root = DOM.createElement('IFRAME');

    // parse on load
    root.onload = function(){
      //console.dir(root.contentWindow);
      var document = this.contentWindow.document;

      // write html
      document.write(html.replace(/\b(src|href)\s*=\s*("[^"]+"|'[^']+'|[^\s\>]+)/g, ''));

      // walk through all nodes and search for elements with className
      var walker = new DOM.TreeWalker(document);
      var map = {};
      var node;

      while (node = walker.next())
      {
        if (node.nodeType == 1 && node.className)
        {
          node.className.qw().forEach(function(token){
            map[token] = true;
          });
        }
      }

      // result is unique class tokens
      var names = Object.keys(map);
      callback(path, names);
    }

    // triggering iframe loading
    DOM.remove(DOM.insert(document.body, root));
  }

  function getNamesFromBemjson(text, path, callback){

    function addMods(baseName, mods){
      if (mods)
        for (var key in mods)
          names[baseName + '_' + key + '_' + mods[key]] = true;
    }

    var names = {};
    var queue = [text.toObject(true)];
    var name;
    var obj;
    while (obj = queue.pop())
    {
      if (obj && typeof obj != 'string' && typeof obj != 'number' && typeof obj != 'boolean')
      {
        if (Array.isArray(obj))
          queue.push.apply(queue, obj);
        else
        {
          name = obj.block;
          if (name)
          {
            names[name] = true;
            addMods(name, obj.mods);

            if (obj.elem)
            {
              name += '__' + obj.elem;
              names[name] = true;
              addMods(name, obj.elemMods);
            }
          }
        
          queue.push(obj.mix, obj.content);
        }
      }
    }      

    callback(path, Object.keys(names));
  }

  function createDeclaration(path, names){
    var techId = path + '.bemdecl.js';
    var decl = names2decl(names);

    var tech = App.Type.Tech({
      id: techId,
      content: 'exports.blocks = ' + JSON.stringify(decl) + ';'
    });

    logResult('Декларация <b>' + tech.getId() + '</b> создана.');

    App.UrlResolver.setTarget(tech.getId());
  }

  //
  // Commands
  //

  var commands = {
    help: function(){
      throw 'Command is not ready yet';
    },
    create: function(env){

      function createBem(bemType, path, noMessageOnExists){
        var bemType = App.Type.resolveBemType(path);
        //console.log(path, ' = ', bemType);

        //if (!bemType || bemType !== checkType)
        //  throw '<b>' + path + '</b> - неверный тип.';

        var object = bemType.get(path);

        if (!object)
        {
          object = bemType.create ? bemType.create(path) : bemType(path);
          logResult('Объект <b>' + object.getId() + '</b> создан.');
        }
        else
        {
          if (!noMessageOnExists)
            logResult('Объект <b>' + object.getId() + '</b> был создан ранее, инструкция проигнорирована.');
        }

        return object;
      }

      function createTechs(path){
        if (env.tokens['-t'])
          return env.tokens['-t'].map(function(techType){
            return createBem(App.Type.Tech, path + '.' + techType);
          }).pop();
      }

      var args = env.args;
      var what = args.shift();
      var names = args;
      var name;
      var last;
      var path = buildBemPath(env);

      if (!what)
        throw 'Для команды create не задан тип создаваемой сущности. Возможные значения: level, block, elem, mod, tech, bemdecl.'

      if (!names.length && what != 'bemdecl')
        throw 'Для команды create не указано имя (имен) создаваемой сущности.';

      if (what != 'level' && !path)
        throw 'Не задан путь (задается ключом -l)';

      switch (what) {
        case 'level':
          while (name = args.shift())
            App.Type.Level(name);
        break;

        case 'block':
          var owner = getOwner(env);

          if (owner.type != App.Type.Block && owner.type != App.Type.Level)
            throw 'Не блок и не уровень';

          var path = owner.getId();
          while (name = args.shift())
          {
            var id = path + '/' + name;
            last = createBem(App.Type.Block, id, true);
            //last = createTechs(id) || last;
          }
        break;

        case 'elem':
          var owner = getOwner(env);

          if (owner.type != App.Type.Block)
            throw 'Не блок';

          var path = owner.getId();
          while (name = args.shift())
          {
            var id = path + '__' + name;
            last = createBem(App.Type.Elem, id, true);
            //last = createTechs(id) || last;
          }
        break;

        case 'mod':
          var values = env.tokens['-v'] || [''];
          //if (!values)
          //  throw 'Для создаваемого модификатора не задано ни одно значение (-v).';

          var owner = getOwner(env);

          if (owner.type != App.Type.Block && owner.type != App.Type.Elem)
            throw 'Не блок и не элемент';

          var path = owner.getId();
          while (name = args.shift())
          {
            for (var i = 0; i < values.length; i++)
            {
              var value = values[i];
              var id = path + '_' + name + (value ? '_' + value : value);

              last = createBem(App.Type.Mod, id, true);
              //last = createTechs(id) || last;
            }
          }
        break;

        case 'tech':
          var owner = getOwner(env);

          if (owner.type != App.Type.Block && owner.type != App.Type.Elem && owner.type != App.Type.Mod)
            throw 'Не блок, не элемент и не модификатор';

          var path = owner.getId();
          while (name = args.shift())
          {
            var id = path + '.' + name;
            last = createBem(App.Type.Tech, id, true);
          }
        break;

        case 'bemdecl':
          if (!env.tokens['-t'])
            throw 'Не указана технология';

          var owner = getOwner(env);
          var ownerId = owner.getId();
          var namesGetter;
          var techId;

          switch (env.tokens['-t'][0]){
            case 'html':
              techId = ownerId + '.html';
              namesGetter = getNamesFromHtml;
            break;
            case 'bemjson.js':
              techId = ownerId + '.bemjson.js';
              namesGetter = getNamesFromBemjson;
            break;
          }

          var tech = App.Type.Tech.get(techId);

          if (!tech)
            throw 'Не найдена технология для создания декларации (' + techId + ')';

          namesGetter(tech.data.content, ownerId, createDeclaration);
        break;

        default:
          throw 'Неверное значение для типа создаваемой сущности. Возможные значения: level, block, elem, mod, tech.';
      }

      ;;;console.log(last);

      if (last)
      {
        App.UrlResolver.setTarget(last.getId());
        if (env.tokens['-go'] && (last.type == App.Type.Block || last.type == App.Type.Level))
          App.UrlResolver.setContext(last.getId());
      }
    },
    'delete': function(env){
      var args = env.args;
      var what = args.shift();
      var names = args;
      var name;
      var last;

      if (!what)
        throw 'Для команды delete не задан тип удаляемой сущности. Возможные значения: level, block, elem, mod.'

      if (!names.length && what != 'bemdecl')
        throw 'Для команды delete не указано имя (имен) удаляемой сущности.';
      
      // determine location
      var lTokens = env.tokens['-l'];
      var path = curLocation;
      if (lTokens)
      {
        if (lTokens.length > 1)
          throw 'Можно задать только значение для ключа -l.';

        path += lTokens[0].replace(/^\/+|\/+$/g, '');
      }

      if (what != 'level' && !path)
        throw 'Не задан путь (задается ключом -l)';

      var ownerId = getOwner(env).getId();

      var what2type = {
        level: App.Type.Level,
        block: App.Type.Block,
        elem: App.Type.Elem,
        mod: App.Type.Mod,
        tech: App.Type.Tech
      };
      var type = what2type[what];

      if (!type)
        throw 'Неверный тип - ' + type;

      var whatPrefix = {
        level: '/',
        block: '/',
        elem: '__',
        mod: '_',
        tech: '.'
      };

      for (var i = 0; i < names.length; i++)
      {
        var objectId = ownerId + whatPrefix[what] + names[i];
        var object = type.get(objectId);
        if (object)
        {
          object.getSubtree().sort().reverse().forEach(function(item){
            item.destroy();
            logResult('Объект <b>' + item.getId() + '</b> удален.');
          });
        }
        else
          logResult('Объект <b>' + objectId + '</b> не найден.')
      }

    },
    rename: function(env){
      var args = env.args;
      var what = args.shift();
      var names = args;
      var name;
      var last;

      if (!what)
        throw 'Для команды delete не задан тип удаляемой сущности. Возможные значения: level, block, elem, mod.'

      if (!names.length && what != 'bemdecl')
        throw 'Для команды delete не указано имя (имен) удаляемой сущности.';
      
      // determine location
      var lTokens = env.tokens['-l'];
      var path = curLocation;
      if (lTokens)
      {
        if (lTokens.length > 1)
          throw 'Можно задать только значение для ключа -l.';

        path += lTokens[0].replace(/^\/+|\/+$/g, '');
      }

      if (what != 'level' && !path)
        throw 'Не задан путь (задается ключом -l)';

      var ownerId = getOwner(env).getId();

      var what2type = {
        level: App.Type.Level,
        block: App.Type.Block,
        elem: App.Type.Elem,
        mod: App.Type.Mod,
        tech: App.Type.Tech
      };
      var type = what2type[what];

      if (!type)
        throw 'Неверный тип - ' + type;

      var whatPrefix = {
        level: '/',
        block: '/',
        elem: '__',
        mod: '_',
        tech: '.'
      };

      getOwner().rename(names[0]);

    },
    cd: function(env){
      var location = env.args.shift();

      var path;
      var prev;
      if (location)
      {
        path = (location.indexOf('/') == 0 ? '' : curLocation);  // if location starts with / - absolute path
        path += location.replace(/^\/?|\/?$/g, '/');  // set / at the begin and at the end

        do // process for ..
        {
          prev = path;
          path = path.replace(/[^/]+\/\.\.\//, '');
        } while (path != prev);

        path = path.replace(/^\/|\/$/g, ''); // remove / from the start and the end

        if (path && !App.Type.resolveBem(path))  // try resolve path
          throw 'Not found - ' + path;

        App.UrlResolver.setContext(path);

        //getOwner(env).getId();
        //curLocation = '/';
      }
      logResult('Текущая локация - ' + (path || curLocation));
    },
    build: function(){
      throw 'Command is not ready yet';
    }
  }

  function parse(str){
    var result = {
      command: null,
      args: [],
      tokens: {},
      techs: [],
      values: [],
      block: null,
      level: null,
      element: null
    };

    var parts = str.trim().qw();

    result.command = parts.shift();

    if (!result.command)
      throw 'Не указано имя команды';

    var tokens = result.tokens;
    var token;
    var arg;
    var array;
    while (parts.length){
      token = parts.shift();
      if (/^-(go)$/.test(token))
      {
        tokens[token] = true;
      }
      else
        if (/^-[bemltdov]$/.test(token))
        {
          arg = parts.shift();

          if (!arg)
            throw 'Не найдено значение для ключа ' + token;

          if (!tokens[token])
            tokens[token] = [arg];
          else
            tokens[token].push(arg);
        }
        else
          result.args.push(token);
    }

    return result;
  }

  function run(str, rethrow){
    var env = parse(str);
    var command = commands[env.command];
    ///console.log(str, command)

    logCommand(str);

    try {
      if (command)
        command(env);
      else
        throw 'Неизвестная команда ' + env.command + '. Поддерживаемые команды: ' + Object.keys(commands).sort().join(', ');
    } catch(e) {
      if (rethrow)
        throw e;
      console.log(str + '\nError: ', e);
    }
  }

  App.UrlResolver.addHandler({
    contextChanged: function(context){
      curLocation = context;
    }
  })

  Basis.namespace(namespace).extend({
    parser: {
      run: run
    }
  });
    
})();